Day 21: JDBC 데이터베이스
JDBC(Java Database Connectivity)는 Java에서 데이터베이스에 접속하고 SQL을 실행하는 표준 API입니다. 다양한 데이터베이스(MySQL, PostgreSQL, Oracle, H2 등)를 동일한 인터페이스로 다룰 수 있습니다. 여기서는 가볍고 설정이 필요 없는 H2 인메모리 데이터베이스로 실습합니다.
데이터베이스 연결과 테이블 생성
JDBC로 데이터베이스에 연결하고 테이블을 만듭니다.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class JdbcConnection {
// H2 인메모리 DB URL
private static final String DB_URL = "jdbc:h2:mem:testdb";
private static final String DB_USER = "sa";
private static final String DB_PASSWORD = "";
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
}
public static void main(String[] args) {
// try-with-resources로 자동 리소스 해제
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
System.out.println("DB 연결 성공!");
System.out.println("DB 제품: " + conn.getMetaData().getDatabaseProductName());
// 테이블 생성
String createTable = """
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE,
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""";
stmt.execute(createTable);
System.out.println("users 테이블 생성 완료");
} catch (SQLException e) {
System.err.println("DB 에러: " + e.getMessage());
}
}
}
CRUD 작업 (PreparedStatement)
SQL 인젝션을 방지하는 PreparedStatement로 데이터를 다룹니다.
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
record User(int id, String name, String email, int age) {}
public class JdbcCrud {
private static final String URL = "jdbc:h2:mem:testdb";
static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, "sa", "");
}
// CREATE
static int insertUser(Connection conn, String name, String email, int age)
throws SQLException {
String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, name);
pstmt.setString(2, email);
pstmt.setInt(3, age);
pstmt.executeUpdate();
try (ResultSet keys = pstmt.getGeneratedKeys()) {
if (keys.next()) return keys.getInt(1);
}
}
return -1;
}
// READ (전체)
static List<User> findAllUsers(Connection conn) throws SQLException {
String sql = "SELECT id, name, email, age FROM users ORDER BY id";
List<User> users = new ArrayList<>();
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
users.add(new User(
rs.getInt("id"),
rs.getString("name"),
rs.getString("email"),
rs.getInt("age")
));
}
}
return users;
}
// READ (조건)
static User findById(Connection conn, int id) throws SQLException {
String sql = "SELECT id, name, email, age FROM users WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return new User(rs.getInt("id"), rs.getString("name"),
rs.getString("email"), rs.getInt("age"));
}
}
}
return null;
}
// UPDATE
static boolean updateUser(Connection conn, int id, String name, int age)
throws SQLException {
String sql = "UPDATE users SET name = ?, age = ? WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, name);
pstmt.setInt(2, age);
pstmt.setInt(3, id);
return pstmt.executeUpdate() > 0;
}
}
// DELETE
static boolean deleteUser(Connection conn, int id) throws SQLException {
String sql = "DELETE FROM users WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
return pstmt.executeUpdate() > 0;
}
}
public static void main(String[] args) throws SQLException {
try (Connection conn = getConnection()) {
// 테이블 생성
conn.createStatement().execute("""
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50), email VARCHAR(100), age INT)
""");
// INSERT
insertUser(conn, "홍길동", "hong@test.com", 25);
insertUser(conn, "김영희", "kim@test.com", 30);
insertUser(conn, "이철수", "lee@test.com", 28);
// SELECT ALL
System.out.println("=== 전체 목록 ===");
findAllUsers(conn).forEach(System.out::println);
// UPDATE
updateUser(conn, 1, "홍길동(수정)", 26);
// SELECT ONE
System.out.println("\n수정 후: " + findById(conn, 1));
// DELETE
deleteUser(conn, 2);
System.out.println("\n=== 삭제 후 ===");
findAllUsers(conn).forEach(System.out::println);
}
}
}
트랜잭션 처리
여러 SQL 작업을 하나의 원자적 단위로 처리합니다.
import java.sql.*;
public class TransactionExample {
private static final String URL = "jdbc:h2:mem:bankdb";
static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, "sa", "");
}
static void transfer(Connection conn, int fromId, int toId, long amount)
throws SQLException {
// 트랜잭션 시작
conn.setAutoCommit(false);
try {
// 출금
String withdrawSql = "UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?";
try (PreparedStatement pstmt = conn.prepareStatement(withdrawSql)) {
pstmt.setLong(1, amount);
pstmt.setInt(2, fromId);
pstmt.setLong(3, amount);
int rows = pstmt.executeUpdate();
if (rows == 0) {
throw new SQLException("출금 실패: 잔액 부족 또는 계좌 없음");
}
}
// 입금
String depositSql = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(depositSql)) {
pstmt.setLong(1, amount);
pstmt.setInt(2, toId);
int rows = pstmt.executeUpdate();
if (rows == 0) {
throw new SQLException("입금 실패: 계좌 없음");
}
}
// 이체 기록
String logSql = "INSERT INTO transfer_log (from_id, to_id, amount) VALUES (?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(logSql)) {
pstmt.setInt(1, fromId);
pstmt.setInt(2, toId);
pstmt.setLong(3, amount);
pstmt.executeUpdate();
}
conn.commit(); // 모두 성공하면 커밋
System.out.println("이체 성공: " + fromId + " -> " + toId + " (" + amount + "원)");
} catch (SQLException e) {
conn.rollback(); // 하나라도 실패하면 롤백
System.err.println("이체 실패 (롤백): " + e.getMessage());
} finally {
conn.setAutoCommit(true);
}
}
public static void main(String[] args) throws SQLException {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
stmt.execute("""
CREATE TABLE accounts (
id INT PRIMARY KEY, name VARCHAR(50), balance BIGINT)
""");
stmt.execute("""
CREATE TABLE transfer_log (
id INT AUTO_INCREMENT PRIMARY KEY,
from_id INT, to_id INT, amount BIGINT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)
""");
stmt.execute("INSERT INTO accounts VALUES (1, '홍길동', 100000)");
stmt.execute("INSERT INTO accounts VALUES (2, '김영희', 50000)");
System.out.println("=== 이체 전 ===");
printAccounts(conn);
transfer(conn, 1, 2, 30000);
System.out.println("\n=== 이체 후 ===");
printAccounts(conn);
// 잔액 부족 시도
transfer(conn, 1, 2, 999999);
}
}
static void printAccounts(Connection conn) throws SQLException {
try (ResultSet rs = conn.createStatement()
.executeQuery("SELECT * FROM accounts")) {
while (rs.next()) {
System.out.printf("%s (ID:%d): %,d원%n",
rs.getString("name"), rs.getInt("id"), rs.getLong("balance"));
}
}
}
}
DAO 패턴
데이터 접근 로직을 별도 클래스로 분리하는 패턴입니다.
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
record Product(int id, String name, int price, int stock) {}
// DAO 인터페이스
interface ProductDao {
int save(Product product);
Optional<Product> findById(int id);
List<Product> findAll();
boolean update(Product product);
boolean delete(int id);
}
// JDBC 구현
class JdbcProductDao implements ProductDao {
private final Connection conn;
JdbcProductDao(Connection conn) {
this.conn = conn;
}
@Override
public int save(Product product) {
String sql = "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, product.name());
ps.setInt(2, product.price());
ps.setInt(3, product.stock());
ps.executeUpdate();
try (ResultSet keys = ps.getGeneratedKeys()) {
return keys.next() ? keys.getInt(1) : -1;
}
} catch (SQLException e) {
throw new RuntimeException("저장 실패", e);
}
}
@Override
public Optional<Product> findById(int id) {
String sql = "SELECT * FROM products WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) return Optional.of(mapRow(rs));
}
} catch (SQLException e) {
throw new RuntimeException("조회 실패", e);
}
return Optional.empty();
}
@Override
public List<Product> findAll() {
List<Product> list = new ArrayList<>();
try (ResultSet rs = conn.createStatement()
.executeQuery("SELECT * FROM products")) {
while (rs.next()) list.add(mapRow(rs));
} catch (SQLException e) {
throw new RuntimeException("전체 조회 실패", e);
}
return list;
}
@Override
public boolean update(Product p) {
String sql = "UPDATE products SET name=?, price=?, stock=? WHERE id=?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, p.name());
ps.setInt(2, p.price());
ps.setInt(3, p.stock());
ps.setInt(4, p.id());
return ps.executeUpdate() > 0;
} catch (SQLException e) {
throw new RuntimeException("수정 실패", e);
}
}
@Override
public boolean delete(int id) {
try (PreparedStatement ps = conn.prepareStatement(
"DELETE FROM products WHERE id=?")) {
ps.setInt(1, id);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
throw new RuntimeException("삭제 실패", e);
}
}
private Product mapRow(ResultSet rs) throws SQLException {
return new Product(
rs.getInt("id"), rs.getString("name"),
rs.getInt("price"), rs.getInt("stock"));
}
}
오늘의 연습문제
-
게시판 DB:
posts테이블(id, title, content, author, created_at)을 만들고 CRUD 메서드를 구현하세요. 제목으로 검색하는findByTitle(String keyword)메서드도 추가하세요. -
배치 삽입:
PreparedStatement의addBatch()와executeBatch()를 사용하여 1000개의 레코드를 한 번에 삽입하는 프로그램을 작성하세요. 건별 삽입과 배치 삽입의 성능을 비교하세요. -
계좌 이체 시스템: 3개의 계좌 간 이체를 트랜잭션으로 처리하는 프로그램을 작성하세요. A->B 이체 중 에러가 발생하면 롤백되는 것을 확인하고, 이체 이력을 별도 테이블에 기록하세요.